//	--------------------------------------------------------------------
//	Copyright(C) 2006,2007 Zhao Yukun. All rights reserved.
//	This file is a part of the Xeres system.
//	Xeres 2007
//	Contact : xeres.engine@gmail.com
//	$(reservedInfo)
//	$(reservedDeclaration)
//	--------------------------------------------------------------------
///	@file	<ctrl_edit.cpp>
///	@path	~/src/appear/ctrl/
///	@date	2007/12/14
///	@desc	Simple edit control implementation.

#define _FULLAPI
#include "config/config.h"

#include "lib/math/math_win32.h"

#include "appear/ctrl/ctrl_edit.h"

namespace xeres {

	// CtrlEdit
	CtrlEdit::CtrlEdit( void )
		: m_nBorder( 3 )
		, m_nSpacing( 0 )
		, m_nCaret( 0 )
		, m_nSelStart( 0 )
		, m_nFirstVisible( 0 )
		, m_bCaretOn( true )
		, m_bEntered( false )
		, m_bInsertMode( true )
		, m_bMenuLoop( false )
		, m_bActive( false )
		, m_bMouseDrag( false )
		, m_isPassword( false )
		, m_bHideCaret( false )
		, m_needUpdate( true )
		, m_buffer( NULL )
	{
		m_hotkey.SetKey( Input::INPUT_STATE_CTRL , KeyInput::KEY_A ,
			bind( &CtrlEdit::SelectAll , this ) );

		m_hotkey.SetKey( Input::INPUT_STATE_CTRL , KeyInput::KEY_C ,
			bind( &CtrlEdit::CopyToClipboard , this ) );

		m_hotkey.SetKey( Input::INPUT_STATE_CTRL , KeyInput::KEY_V ,
			bind( &CtrlEdit::PasteFromClipboard , this ) );

		m_hotkey.SetKey( Input::INPUT_STATE_CTRL , KeyInput::KEY_X ,
			bind( &CtrlEdit::Cut , this ) );
	}

	// ~CtrlEdit
	CtrlEdit::~CtrlEdit( void )
	{
		m_buffer.Clear();
		::DeleteObject( m_font );
			m_font = NULL;
	}

	// TextLeft
	inline int CtrlEdit::TextLeft( void ) const
	{
		return m_rect.left() + 4;
	}

	// TextTop
	inline int CtrlEdit::TextTop( void ) const
	{
		return m_rect.top() + 3;
	}

	// TextRight
	inline int CtrlEdit::TextRight( void ) const
	{
		return m_rect.right() - 10;
	}

	// TextBottom
	inline int CtrlEdit::TextBottom( void ) const
	{
		return m_rect.bottom() - 3;
	}

	// SetFont
	void CtrlEdit::SetFont( RefWeak<IFont> font )
	{
		if( m_font )
		{
			::DeleteObject( m_font );
			m_font = NULL;
		}
		HDC dc = (HDC)font->GetDC();
		HFONT f =(HFONT)::GetCurrentObject( dc , OBJ_FONT );
		LOGFONT lf;
		::GetObject( f , sizeof(LOGFONT) , &lf );
		m_font = ::CreateFontIndirect( &lf );

		m_buffer.SetFont( f );
	}

	// OnSetFocus
	void CtrlEdit::OnSetFocus( void )
	{
		m_bActive = true;
		ResetCaretBlink( true );
	}

	// OnKillFocus
	void CtrlEdit::OnKillFocus( void )
	{
		m_bActive = false;
	}

	// OnMouseInput
	HandleInput CtrlEdit::OnMouseInput( const MouseInput& input , int x , int y )
	{
		switch( input.GetAction() )
		{
		case Input::ACTION_MOUSE_LB_CLICK:
			OnLMBDown( input.GetState() , x , y );
			return INPUT_HANDLED;
		case Input::ACTION_MOUSE_LB_UP:
			OnLMBUp();
			return INPUT_HANDLED;
		case Input::ACTION_MOUSE_MOVE:
			OnMouseMove( input.GetState() , x , y );
			return INPUT_HANDLED;
		case Input::ACTION_MOUSE_LB_DBCLICK:
			OnLMBDblClk();
			return INPUT_HANDLED;
		}
		return INPUT_UNHANDLED;
	}

	// OnLMBDown
	void CtrlEdit::OnLMBDown( uint32 status , int x , int y )
	{
		m_bMouseDrag = true;

		// Determine the character corresponding to the coordinates.
		int nCP, nTrail, nX1st;
		m_buffer.CPtoX( m_nFirstVisible, FALSE, &nX1st );  // X offset of the 1st visible char

		POINT pt = { x , y };

		int left = TextLeft();

		if( SUCCEEDED( m_buffer.XtoCP( pt.x - left + nX1st, &nCP, &nTrail ) ) )
		{
			// Cap at the NULL character.
			if( nTrail && nCP < (int)m_buffer.GetTextSize() )
				PlaceCaret( nCP + 1 );
			else
				PlaceCaret( nCP );
			if( !( status & Input::INPUT_STATE_SHIFT ) )
			{
				m_nSelStart = m_nCaret;
			}
			ResetCaretBlink();
		}
	}

	// OnLMBUp
	void CtrlEdit::OnLMBUp( void )
	{
		m_bMouseDrag = false;
	}

	// OnLMBDblClk
	void CtrlEdit::OnLMBDblClk( void )
	{
		if( m_nSelStart == m_nCaret )
		{
			m_nSelStart = 0;
			PlaceCaret( (int)m_buffer.GetTextSize() );
		}
	}

	// OnMouseLeave
	void CtrlEdit::OnMouseLeave( void )
	{
		m_bEntered = false;
	}

	// OnMouseMove
	void CtrlEdit::OnMouseMove( uint32 status , int x , int y )
	{
		if( m_bMouseDrag )
		{
			POINT pt = { x , y };
			// Determine the character corresponding to the coordinates.
			int nCP, nTrail, nX1st;
			m_buffer.CPtoX( m_nFirstVisible, FALSE, &nX1st );  // X offset of the 1st visible char

			int left = TextLeft();

			if( SUCCEEDED( m_buffer.XtoCP( pt.x - left + nX1st, &nCP, &nTrail ) ) )
			{
				// Cap at the NULL character.
				if( nTrail && nCP < (int)m_buffer.GetTextSize() )
					PlaceCaret( nCP + 1 );
				else
					PlaceCaret( nCP );
			}
		}
	}

	// OnKeyDown
	bool CtrlEdit::OnKeyDown( const KeyInput& input )
	{
		if( !input.IsPressed() )
			return false;

		LockGuard<ThreadLock> locker(m_lock);

		// short cuts
		if( m_hotkey.OnKeyInput( input ) == INPUT_HANDLED )
			return INPUT_HANDLED;

		switch( input.GetKey() )
		{
		case KeyInput::KEY_HOME:
			PlaceCaret( 0 );
			if( !input.IsShiftDown() )
				m_nSelStart = m_nCaret;
			ResetCaretBlink();
			return true;

		case KeyInput::KEY_END:
			PlaceCaret( (int)m_buffer.GetTextSize() );
			if( !input.IsShiftDown() )
				m_nSelStart = m_nCaret;
			ResetCaretBlink();
			return true;

		case KeyInput::KEY_INSERT:
			if( input.IsCtrlDown() )
			{
				// Control + Insert. Copy to clipboard.
				CopyToClipboard();
			}
			else if( input.IsShiftDown() )
			{
				// Shift + Insert. Paste from clipboard.
				PasteFromClipboard();
			}
			else
			{
				// Toggle caret insert mode.
				m_bInsertMode = !m_bInsertMode;
				m_needUpdate = true;
			}
			return true;

		case KeyInput::KEY_DELETE:
			// Check if there is a text selection.
			if( m_nCaret != m_nSelStart )
			{
				DeleteSelectionText();
			}
			else
			{
				 m_buffer.RemoveChar( m_nCaret );
			}
			ResetCaretBlink();
			return true;

		case KeyInput::KEY_NUMPAD4:
			if( input.IsNumLocked() )
				return ProcessInput( input );
			// as left arrow if num lock is off
		case KeyInput::KEY_LEFTARROW:
			if( input.IsCtrlDown() )
			{
				// Control is down. Move the caret to a new item
				// instead of a character.
				m_buffer.GetPriorItemPos( m_nCaret, &m_nCaret );
				PlaceCaret( m_nCaret );
			}
			else if( m_nCaret > 0 )
			{
				PlaceCaret( m_nCaret - 1 );
			}
			if( !input.IsShiftDown() )
			{
				// Shift is not down. Update selection
				// start along with the caret.
				m_nSelStart = m_nCaret;
			}
			ResetCaretBlink();
			return true;

		case KeyInput::KEY_NUMPAD6:
			if( input.IsNumLocked() )
				return ProcessInput( input );
			// as left arrow if num lock is off
		case KeyInput::KEY_RIGHTARROW:
			if( input.IsCtrlDown() )
			{
				// Control is down. Move the caret to a new item
				// instead of a character.
				m_buffer.GetNextItemPos( m_nCaret, &m_nCaret );
				PlaceCaret( m_nCaret );
			}
			else if( m_nCaret < (int)m_buffer.GetTextSize() )
			{
				PlaceCaret( m_nCaret + 1 );
			}
			if( !input.IsShiftDown() )
			{
				m_nSelStart = m_nCaret;
			}
			ResetCaretBlink();
			return true;

		case KeyInput::KEY_BACKSPACE:
			// If there's a selection, treat this
			// like a delete key.
			if( m_nCaret != m_nSelStart )
			{
				DeleteSelectionText();
			}
			else if( m_nCaret > 0 )
			{
				// Move the caret, then delete the char.
				PlaceCaret( m_nCaret - 1 );
				m_nSelStart = m_nCaret;
				m_buffer.RemoveChar( m_nCaret );
			}
			ResetCaretBlink();
			break;

		default:
			// try to translate characters & input
			{
				return ProcessInput(input);
			}
		}
		return false;
	}

	// ProcessInput
	bool CtrlEdit::ProcessInput( const KeyInput &input )
	{
		if( input.IsAltDown() || input.IsCtrlDown() )
			return false;

		wchar_t ch = input.GetInputChar();
		if( ch == '\0' )
			return false;

		// If there's a selection and the user
		// starts to type, the selection should
		// be deleted.
		if( m_nCaret != m_nSelStart )
		{// overwrite
			DeleteSelectionText();
			m_nSelStart = m_nCaret;
		}

		if( !m_bInsertMode && ( m_nCaret < (int)m_buffer.GetTextSize() ) )
		{
			m_buffer[m_nCaret] = ch;
			PlaceCaret( m_nCaret + 1 );
			m_nSelStart = m_nCaret;
		}
		else
		{
			// If we are in overwrite mode and there is already
			// a char at the caret's position, simply replace it.
			// Otherwise, we insert the char as normal.
			if( m_buffer.InsertChar( m_nCaret, ch ) )
			{
				PlaceCaret( m_nCaret + 1 );
				m_nSelStart = m_nCaret;
			}
		}
		ResetCaretBlink();
		return true;
	}

	// OnCtrlSize
	void CtrlEdit::OnCtrlSize( const Rect<int>& rect )
	{
		m_rect = rect;
		m_nFirstVisible = 0;
		m_needUpdate = true;
	}

	// SetPassword
	void CtrlEdit::SetPassword( void )
	{
		LockGuard<ThreadLock> locker(m_lock);

		m_isPassword = true;
		m_needUpdate = true;
	}

	// SelectAll
	void CtrlEdit::SelectAll( void )
	{
		LockGuard<ThreadLock> locker(m_lock);

		m_nSelStart = 0;
		PlaceCaret( (int)m_buffer.GetTextSize() );
	}

	// SetCaretTimer
	void CtrlEdit::SetCaretTimer( void )
	{

	}

	// ClearText
	void CtrlEdit::ClearText( void )
	{
		LockGuard<ThreadLock> locker(m_lock);

		m_buffer.Clear();
		m_nFirstVisible = 0;
		PlaceCaret( 0 );
		m_nSelStart = 0;
	}

	// SetText
	void CtrlEdit::SetText( const WString& text , bool bSelected )
	{
		LockGuard<ThreadLock> locker(m_lock);

		m_buffer.SetText( text );
		m_nFirstVisible = 0;
		// Move the caret to the end of the text
		PlaceCaret( (int)m_buffer.GetTextSize() );
		m_nSelStart = bSelected ? 0 : m_nCaret;
	}

	// CopyToClipboard
	void CtrlEdit::CopyToClipboard( void )
	{
		// Copy the selection text to the clipboard
		if( m_nCaret != m_nSelStart && ::OpenClipboard( NULL ) )
		{
			::EmptyClipboard();

			HGLOBAL hBlock = ::GlobalAlloc( GMEM_MOVEABLE, sizeof(WCHAR) * ( m_buffer.GetTextSize() + 1 ) );
			if( hBlock )
			{
				WCHAR * pwszText = (WCHAR*)::GlobalLock( hBlock );
				if( pwszText )
				{
					int nFirst = __min( m_nCaret, m_nSelStart );
					int nLast = __max( m_nCaret, m_nSelStart );
					if( nLast - nFirst > 0 )
						::CopyMemory( pwszText, m_buffer.GetBuffer() + nFirst, (nLast - nFirst) * sizeof(WCHAR) );
					pwszText[nLast - nFirst] = 0;  // Terminate it
					::GlobalUnlock( hBlock );
				}
				::SetClipboardData( CF_UNICODETEXT, hBlock );
			}
			::CloseClipboard();
			// We must not free the object until CloseClipboard is called.
			if( hBlock )
				::GlobalFree( hBlock );
		}
	}

	// PasteFromClipboard
	void CtrlEdit::PasteFromClipboard( void )
	{
		if( ::OpenClipboard( NULL ) )
		{
			HANDLE handle = ::GetClipboardData( CF_UNICODETEXT );
			if( handle )
			{
				// Convert the ANSI string to Unicode, then
				// insert to our buffer.
				WCHAR *pwszText = (WCHAR*)::GlobalLock( handle );
				if( pwszText )
				{
					if( wcslen( pwszText ) > 0 )
					{
						int nFirst = __min( m_nCaret, m_nSelStart );
						int nLast = __max( m_nCaret, m_nSelStart );
						// Update caret and selection
						//				PlaceCaret( nFirst );
						//				m_nSelStart = m_nCaret;
						if( m_buffer.InsertString( m_nCaret, pwszText ) )
							PlaceCaret( m_nCaret + lstrlenW( pwszText ) );
						m_nSelStart = m_nCaret;
					}
					::GlobalUnlock( handle );
				}
			}
			::CloseClipboard();
		}
		else
		{
			DeleteSelectionText();
		}
	}

	// Cut
	void CtrlEdit::Cut( void )
	{
		CopyToClipboard();
		ClearText();
	}

	// GetRectText
	void CtrlEdit::GetRectText( RECT& rc )
	{
		rc.left = TextLeft();
		rc.top = TextTop();
		rc.right = TextRight();
		rc.bottom = TextBottom();
	}

	// PlaceCaret
	void CtrlEdit::PlaceCaret( int nCP )
	{
		m_needUpdate = true;

		assert( nCP >= 0 && nCP <= (int)m_buffer.GetTextSize() );
		m_nCaret = nCP;

		RECT rcText;
		GetRectText( rcText );

		// Obtain the X offset of the character.
		int nX1st, nX, nX2;
		m_buffer.CPtoX( m_nFirstVisible, FALSE, &nX1st );  // 1st visible char
		m_buffer.CPtoX( nCP, FALSE, &nX );  // LEAD
		// If nCP is the NULL terminator, get the leading edge instead of trailing.
		if( nCP == m_buffer.GetTextSize() )
			nX2 = nX;
		else
			m_buffer.CPtoX( nCP, TRUE, &nX2 );  // TRAIL

		// If the left edge of the char is smaller than the left edge of the 1st visible char,
		// we need to scroll left until this char is visible.
		if( nX <= nX1st )
		{
			// Simply make the first visible character the char at the new caret position.
			if( nCP > 0 )
				m_nFirstVisible = nCP - 1;
			else
				m_nFirstVisible = nCP;
		}
		// If the right of the character is bigger than the offset of the control's
		// right edge, we need to scroll right to this character.
		else if( nX2 > nX1st + RectWidth( rcText ) )
		{
			// Compute the X of the new left-most pixel
			int nXNewLeft = nX2 - RectWidth( rcText );

			// Compute the char position of this character
			int nCPNew1st, nNewTrail;
			m_buffer.XtoCP( nXNewLeft, &nCPNew1st, &nNewTrail );

			// If this coordinate is not on a character border,
			// start from the next character so that the caret
			// position does not fall outside the text rectangle.
			int nXNew1st;
			m_buffer.CPtoX( nCPNew1st, FALSE, &nXNew1st );
			if( nXNew1st < nXNewLeft )
				++nCPNew1st;

			m_nFirstVisible = nCPNew1st;
		}
	}

	// DeleteSelectionText
	void CtrlEdit::DeleteSelectionText( void )
	{
		m_needUpdate = true;

		int nFirst = __min( m_nCaret, m_nSelStart );
		int nLast = __max( m_nCaret, m_nSelStart );
		// Update caret and selection
		PlaceCaret( nFirst );
		m_nSelStart = m_nCaret;
		// Remove the characters
		for( int i = nFirst; i < nLast; ++i )
			m_buffer.RemoveChar( nFirst );
	}

	// ResetCaretBlink
	void CtrlEdit::ResetCaretBlink( bool ResetTimer /*= false */ )
	{
		m_needUpdate = true;
		m_bCaretOn = true;
	}

	// OnPaint
	void CtrlEdit::OnPaint( Layer& layer )
	{
		LockGuard<ThreadLock> locker(m_lock);

		//
		// Compute the X coordinates of the first visible character.
		//
		int nXFirst = 0;
		m_buffer.CPtoX( m_nFirstVisible, FALSE, &nXFirst );

		int nSelStartX = 0, nCaretX = 0;  // Left and right X coordinates of the selection region

		//
		// Compute the X coordinates of the selection rectangle
		//
		m_buffer.CPtoX( m_nCaret, FALSE, &nCaretX );
		if( m_nCaret != m_nSelStart )
			m_buffer.CPtoX( m_nSelStart, FALSE, &nSelStartX );
		else
			nSelStartX = nCaretX;

		//
		// compute the selection rectangle
		//
		RECT rcSelection = { 0 , 0 , 0 , 0 };  // Make this available for rendering selected text
		if( m_nCaret != m_nSelStart )
		{
			int nSelLeftX = nCaretX, nSelRightX = nSelStartX;
			// Swap if left is bigger than right
			if( nSelLeftX > nSelRightX )
			{
				int nTemp = nSelLeftX;
				nSelLeftX = nSelRightX;
				nSelRightX = nTemp;
			}
			::SetRect( &rcSelection, nSelLeftX, TextTop(), nSelRightX , TextBottom() );
			OffsetRect( &rcSelection, TextLeft() - nXFirst, 0 );
			rcSelection.top += 2;
			rcSelection.bottom -= 2;
		}

		// Start the rectangle with insert mode caret
		RECT rcCaret = { TextLeft() - nXFirst + nCaretX , TextTop() + 1,
			TextLeft() - nXFirst + nCaretX + 2, TextBottom() - 1 };

		if( !m_bInsertMode )
		{
			// Obtain the right edge X coord of the current character
			int nRightEdgeX;
			m_buffer.CPtoX( m_nCaret, TRUE, &nRightEdgeX );
			rcCaret.right = TextLeft() - nXFirst + nRightEdgeX;
		}

		// If we are in overwrite mode, adjust the caret rectangle
		// to fill the entire character.
		if( !m_bInsertMode )
		{
			// Obtain the right edge X coord of the current character
			int nRightEdgeX;
			m_buffer.CPtoX( m_nCaret, TRUE, &nRightEdgeX );
			rcCaret.right = TextLeft() - nXFirst + nRightEdgeX;
		}

		ARGB sel;
		sel.r = 80; sel.g = 100; sel.b = 111;
		sel.a = 166;
		layer.Fill( PackColor4( sel.argb ) , rcSelection.left , rcSelection.top , RectWidth( rcSelection ) , RectHeight( rcSelection ) );

		ARGB caret;
		caret.r = caret.g = caret.b = 200;
		caret.a = 255;
		layer.Fill( PackColor4( caret.argb ) , rcCaret.left , rcCaret.top , RectWidth( rcCaret ) , RectHeight( rcCaret ) );

		ARGB col;
		col.r = col.g = col.b = col.a = 255;
		RECT rb = { TextLeft() , TextTop() , TextRight() , TextBottom() };
		RECT rt = { m_rect.left() , m_rect.top() , m_rect.right() , m_rect.bottom() };
		layer.DrawText( col , m_font , GetText().c_str() + m_nFirstVisible , -1 ,
			rb , rt , DT_LEFT| DT_VCENTER | DT_SINGLELINE | DT_EXPANDTABS );

		m_needUpdate = false;
	}

} // namespace xeres
